﻿using System.Runtime.Serialization;
using System.Windows;
using System.Windows.Data;
using System;
using System.Reflection;
using System.Windows.Documents;
using System.Collections.Generic;
using System.Threading;
using System.Collections;
using System.Xml;
using System.IO;
using System.ComponentModel;

namespace GA.Shared.Interfaces
{
    public abstract class LinkedDependencyObject : DependencyObject
    {
        public static event DependencyPropertyChangedEventHandler PropertyChangedEventHandler;

        /// <summary>
        /// Default constructor.
        /// </summary>
        public LinkedDependencyObject()
        {
            sLinkedProperties = GetAllDependencyProperties();
        }

        /// <summary>
        /// Constructor for linking all properties together.
        /// </summary>
        /// <param name="baseObject">LinkedDependencyObject to link to.</param>
        public LinkedDependencyObject(LinkedDependencyObject baseObject)
        {
            if (baseObject == null) throw new ArgumentNullException("baseObject");

            sLinkedProperties = GetAllDependencyProperties();
            CopyAllLinkedValues(baseObject);
            LinkTo(baseObject);
        }

        /// <summary>
        /// Constructor for linking all of the properties together with the specified BindingMode.
        /// </summary>
        /// <param name="baseObject">LinkedDependencyObject to link to.</param>
        /// <param name="bindingMode">BindingMode to link the DependencyProperties from.</param>
        public LinkedDependencyObject(LinkedDependencyObject baseObject, BindingMode bindingMode)
        {
            if (baseObject == null) throw new ArgumentNullException("baseObject");

            sLinkedProperties = GetAllDependencyProperties();
            CopyAllLinkedValues(baseObject);
            LinkTo(baseObject, bindingMode);
        }

        /// <summary>
        /// Constructor for creating a LinkedDependencyObject out of a SerializableTransferObject.
        /// </summary>
        /// <param name="serializableTransferObject">SerializableTransferObject to parse.</param>
        public LinkedDependencyObject(SerializableTransferObject serializableTransferObject)
        {
            if (serializableTransferObject == null) throw new ArgumentNullException("serializableTransferObject");

            IsTransferObject = true;
            foreach (KeyValuePair<string, object> value in serializableTransferObject.Properties)
            {
                PropertyInfo propertyInfo = GetType().GetProperty(value.Key);
                if (propertyInfo != null && propertyInfo.CanWrite)
                {
                    propertyInfo.SetValue(this, value.Value, null);
                }
            }
        }

        /// <summary>
        /// Registers a property
        /// </summary>
        /// <param name="name">Name of the property.</param>
        /// <param name="valueType">Type of the value</param>
        /// <param name="ownerType">Owner type of the value.</param>
        /// <param name="propertyMetadata">PropertyMetadata to pass to the property.</param>
        /// <returns>DependencyProperty registered.</returns>
        protected static DependencyProperty RegisterProperty(string name, Type valueType, Type ownerType, PropertyMetadata propertyMetadata)
        {
            PropertyMetadata newPropertyMetadata = new PropertyMetadata(
                propertyMetadata.DefaultValue,
                propertyMetadata.PropertyChangedCallback ?? new PropertyChangedCallback(DependencyObject_PropertyChanged),
                propertyMetadata.CoerceValueCallback);
            return DependencyProperty.Register(name, valueType, ownerType, newPropertyMetadata);
        }

        /// <summary>
        /// Default event for handling a linked dependency object changed.
        /// </summary>
        /// <param name="sender">DependencyObject sent.</param>
        /// <param name="e">PropertyChangedEventArgs to send.</param>
        protected static void DependencyObject_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            DependencyPropertyChangedEventHandler eventHandler = PropertyChangedEventHandler;

            if (eventHandler != null)
            {
                eventHandler(sender, e);
            }
        }

        /// <summary>
        /// Gets or sets if this was converted from a transfer object.
        /// </summary>
        protected bool IsTransferObject { get; set; }

        protected DependencyProperty[] GetAllDependencyProperties()
        {
            return GetAllDependencyProperties(GetType());
        }

        /// <summary>
        /// Gets all of the DependencyProperty in the static constructors
        /// </summary>
        /// <param name="type">LinkedDependencyObject Type to search.</param>
        /// <returns>Array of DependencyProperty found in the Type.</returns>
        protected DependencyProperty[] GetAllDependencyProperties(Type type)
        {
            if (type == null) throw new ArgumentNullException("type");
            if (!(type.IsAssignableFrom(typeof(LinkedDependencyObject)))) throw new ArgumentException("type can only represent a LinkedDependencyObject.", "type");

            List<DependencyProperty> properties = new List<DependencyProperty>();
            foreach (PropertyInfo propertyInfo in type.GetProperties())
            {
                if (propertyInfo.PropertyType.IsAssignableFrom(typeof(DependencyProperty)) && propertyInfo.GetRawConstantValue() != null)
                {
                    properties.Add((DependencyProperty)propertyInfo.GetRawConstantValue());
                }
            }
            return properties.ToArray();
        }

        /// <summary>
        /// Gets an Array of DependencyProperties
        /// </summary>
        protected virtual DependencyProperty[] LinkedProperties { get { return sLinkedProperties; } }

        /// <summary>
        /// Delegate for sending through the Dispatcher to get a value.
        /// </summary>
        /// <param name="property">DependencyProperty to get.</param>
        protected delegate object GetValueDelegate(DependencyProperty property);

        /// <summary>
        /// Delegate for setting a value through the Dispatcher.
        /// </summary>
        /// <param name="property">DependencyProperty to set.</param>
        /// <param name="value">Value to put into the property.</param>
        protected delegate void SetValueDelegate(DependencyProperty property, object value);

        /// <summary>
        /// Links two LinkedDependencyObjects together by binding the properties together.
        /// </summary>
        /// <param name="baseObject">LinkedDependencyObject to link to this.</param>
        /// <exception cref="ArgumentNullException">Thrown if baseObject is null.</exception>
        /// <exception cref="ArgumentException">Thrown if baseObject is the same as this object.</exception>
        protected void LinkTo(LinkedDependencyObject baseObject)
        {
            LinkTo(baseObject, BindingMode.TwoWay);
        }

        /// <summary>
        /// Links two LinkedDependencyObjects together by binding the properties together.
        /// </summary>
        /// <param name="baseObject">LinkedDependencyObject to link to this.</param>
        /// <param name="bindingMode">BindingMode to connect the object to.</param>
        /// <exception cref="ArgumentNullException">Thrown if baseObject is null.</exception>
        /// <exception cref="ArgumentException">Thrown if baseObject is the same as this object.</exception>
        protected void LinkTo(LinkedDependencyObject baseObject, BindingMode bindingMode)
        {
            if (baseObject == null) throw new ArgumentNullException("baseObject");
            if (baseObject == this) throw new ArgumentException("baseObject must be different than this object.", "baseObject");

            foreach (DependencyProperty property in LinkedProperties)
            {
                Binding binding = new Binding(property.Name);
                binding.Mode = bindingMode;
                binding.Source = baseObject;
                BindingOperations.SetBinding(this, property, binding);
            }
        }

        /// <summary>
        /// Attempts to safely get a value by using the LinkedDependencyObject's Dispatcher if the threads the object is running on is different than the current thread.
        /// </summary>
        /// <param name="property">DependencyProperty to get.</param>
        /// <returns>Object value found in the object.</returns>
        /// <exception cref="ArgumentNullException">Thrown if property is null.</exception>
        protected object SafeGetValue(DependencyProperty property)
        {
            if (property == null) throw new ArgumentNullException("property");

            if (Dispatcher != null && Dispatcher.Thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId)
            {
                object value = null;
                GetValueDelegate getValueDelegate = delegateProperty => { return GetValue(delegateProperty); };
                value = Dispatcher.Invoke(getValueDelegate, new object[] { property });
                return value;
            }
            else
            {
                return GetValue(property);
            }
        }

        /// <summary>
        /// Attempts to safely get a value by using the LinkedDependencyObject's Dispatcher if the threads the object is running on is different than the current thread.
        /// </summary>
        /// <param name="property">DependencyProperty to set.</param>
        /// <param name="value">Object value to set.</param>
        /// <exception cref="ArgumentNullException">Thrown if property is null.</exception>
        protected void SafeSetValue(DependencyProperty property, object value)
        {
            if (property == null) throw new ArgumentNullException("property");

            if (Dispatcher != null && Dispatcher.Thread.ManagedThreadId != Thread.CurrentThread.ManagedThreadId)
            {
                SetValueDelegate setValueDelegate = (delegateProperty, delegateValue) => { SetValue(delegateProperty, delegateValue); };
                Dispatcher.Invoke(setValueDelegate, new object[] { property, value });
            }
            else
            {
                SetValue(property, value);
            }
        }

        /// <summary>
        /// Copies all of the linked values to the new base object.
        /// </summary>
        /// <param name="baseObject">LinkedDependencyObject to copy the information from.</param>
        /// <exception cref="ArgumentNullException">Thrown if baseObject is null.</exception>
        protected void CopyAllLinkedValues(LinkedDependencyObject baseObject)
        {
            if (baseObject == null) throw new ArgumentNullException("baseObject");

            foreach (DependencyProperty property in LinkedProperties)
            {
                DependencyProperty linkedProperty = baseObject.FindDependencyProperty(property.Name);
                if (linkedProperty != null)
                {
                    SafeSetValue(property, baseObject.SafeGetValue(linkedProperty));
                }
            }
        }

        private DependencyProperty[] sLinkedProperties;

        private DependencyProperty FindDependencyProperty(string propertyName)
        {
            foreach (DependencyProperty property in LinkedProperties)
            {
                if (property.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase))
                {
                    return property;
                }
            }
            return null;
        }

        

        private List<FieldInfo> GetSerializableFieldInformation()
        {
            if (!sSerializationLists.ContainsKey(GetType()))
            {
                List<FieldInfo> fieldInformation = new List<FieldInfo>();
                Type typeRover = GetType();
                while (typeRover != null && typeRover.BaseType != typeof(DependencyObject))
                {
                    FieldInfo[] fields = typeRover.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);

                    foreach (FieldInfo fieldInfo in fields)
                    {
                        if (!fieldInfo.IsNotSerialized)
                        {
                            if (!fieldInfo.FieldType.IsSubclassOf(typeof(MulticastDelegate)) && !fieldInfo.FieldType.IsSubclassOf(typeof(Delegate)))
                            {
                                fieldInformation.Add(fieldInfo);
                            }
                        }
                    }

                    typeRover = typeRover.BaseType;
                }
                sSerializationLists.Add(GetType(), fieldInformation);
            }

            return sSerializationLists[GetType()] as List<FieldInfo>;
        }

        private bool IsSerializableDependencyProperty(PropertyDescriptor descriptor)
        {
            if (descriptor.IsReadOnly)
            {
                return false;
            }

            return FindSerializableDependencyProperty(descriptor.Name) != null;
        }

        private object FindSerializableDependencyProperty(string propertyName)
        {
            string propertyPropertyName = propertyName + "Property";
            FieldInfo fieldInfo = null;
            Type typeRover = GetType();

            while (fieldInfo == null && typeRover != typeof(DependencyObject))
            {
                fieldInfo = typeRover.GetField(propertyPropertyName);
                if (fieldInfo != null && fieldInfo.IsNotSerialized)
                {
                    return null;
                }

                typeRover = typeRover.BaseType;
            }

            if (fieldInfo != null)
            {
                return fieldInfo.GetValue(null);
            }
            return null;
        }

        private static Hashtable sSerializationLists = new Hashtable();
    }
}
